package com.lhkj.ct.meta.modules.psychology.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.lhkj.ct.base.config.FileServerConfig;
import com.lhkj.ct.base.enums.DeleteStatus;
import com.lhkj.ct.base.enums.GlobalStatus;
import com.lhkj.ct.base.enums.ReturnStatus;
import com.lhkj.ct.base.exception.BusinessException;
import com.lhkj.ct.base.exception.SystemException;
import com.lhkj.ct.base.exception.file.InvalidExtensionException;
import com.lhkj.ct.base.model.Option;
import com.lhkj.ct.base.utils.ShiroUtil;
import com.lhkj.ct.base.utils.file.FileUploadUtils;
import com.lhkj.ct.base.utils.file.MimeTypeUtils;
import com.lhkj.ct.meta.admin.service.SysDictDataService;
import com.lhkj.ct.meta.modules.common.enums.PatientModule;
import com.lhkj.ct.meta.modules.gauge.common.enums.GaugeType;
import com.lhkj.ct.meta.modules.gauge.mapper.GaugeQuestionMapper;
import com.lhkj.ct.meta.modules.gauge.mapper.ScaleMapper;
import com.lhkj.ct.meta.modules.gauge.model.entity.TblGaugeOption;
import com.lhkj.ct.meta.modules.gauge.model.entity.TblGaugeQuestion;
import com.lhkj.ct.meta.modules.gauge.model.entity.TblScale;
import com.lhkj.ct.meta.modules.patient.model.entity.TblPatient;
import com.lhkj.ct.meta.modules.patient.service.PatientService;
import com.lhkj.ct.meta.modules.psychology.mapper.ScalePsyImportMapper;
import com.lhkj.ct.meta.modules.psychology.model.bo.PatientItem;
import com.lhkj.ct.meta.modules.psychology.model.bo.SubmitGaugeBo;
import com.lhkj.ct.meta.modules.psychology.model.entity.TblGaugeAnswer;
import com.lhkj.ct.meta.modules.psychology.model.entity.TblScalePsyImport;
import com.lhkj.ct.meta.modules.psychology.model.entity.TblScalePsyRecord;
import com.lhkj.ct.meta.modules.psychology.model.entity.TblScaleReport;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 测验导入
 * </p>
 */
@Service
public class PsyImportProcess {

    private final Logger logger = LoggerFactory.getLogger(PsyImportProcess.class);

    private static final int gaugeExcelMaxCol = 8;

    @Resource
    private ScaleMapper scaleMapper;

    @Resource
    private GaugeQuestionMapper questionMapper;

    @Resource
    private ScalePsyImportMapper psyImportMapper;

    @Resource
    private GaugeAnswerService gaugeAnswerService;

    @Resource
    private PatientService patientService;

    @Resource
    private SysDictDataService dictDataService;

    @Resource
    private PsyAnswerService psyAnswerService;

    @Resource
    private PsyRecordService psyRecordService;

    @Resource
    private ScaleReportService reportService;


    @Transactional
    public void importProcess(MultipartFile excel) {
        TblScale scale;
        Sheet sheet;
        int gaugeNameRow = -1;
        String filePath = null;
        Workbook workbook;
        try {
            if (null == excel || excel.isEmpty()) {
                throw new FileNotFoundException("excel文件不存在或为空");
            }
            try {
                switch (Objects.requireNonNull(FilenameUtils.getExtension(excel.getOriginalFilename()))) {
                    case "xlsx":
                        workbook = new XSSFWorkbook(excel.getInputStream());
                        break;
                    case "xls":
                        workbook = new HSSFWorkbook(excel.getInputStream());
                        break;
                    default:
                        throw new BusinessException(ReturnStatus.SC_INTERNAL_SERVER_ERROR, "不支持非excel文件格式");
                }
            } catch (IOException e) {
                throw new BusinessException(ReturnStatus.SC_INTERNAL_SERVER_ERROR, "读取excel文件失败");
            }
            // step 2.读取工作簿、量表名称
            sheet = workbook.getSheetAt(0);
            for (int i = 1; i < sheet.getLastRowNum(); i++) {
                if (sheet.getRow(i).getCell(0).getStringCellValue().equals("量表信息")) {
                    gaugeNameRow = i + 1;// 初始化为 -1 所以需要+1操作
                    break;
                }
            }
            if (gaugeNameRow == -1) {
                throw new BusinessException(ReturnStatus.SC_INTERNAL_SERVER_ERROR, "量表信息读取失败");
            }
            // 获取量表名称
            String gaugeName = Optional.ofNullable(sheet.getRow(gaugeNameRow).getCell(0).getStringCellValue())
                    .orElseThrow(() -> new BusinessException(ReturnStatus.SC_INTERNAL_SERVER_ERROR, "量表信息读取失败"));
            // step 3.根据量表名称获取量表信息
            scale = scaleMapper.selectOne(new LambdaQueryWrapper<TblScale>()
                    .select(TblScale::getId, TblScale::getScaleName, TblScale::getStatus)
                    .eq(TblScale::getScaleName, gaugeName)
                    .eq(TblScale::getDelFlag, DeleteStatus.NORMAL));
            if (null == scale) {
                throw new BusinessException(ReturnStatus.SC_INTERNAL_SERVER_ERROR, "量表不存在或已被删除");
            }
            if (GlobalStatus.DISABLED.equals(scale.getStatus())) {
                throw new BusinessException(ReturnStatus.SC_INTERNAL_SERVER_ERROR, "量表已禁用，请联系管理员");
            }
        } catch (FileNotFoundException e) {
            logger.error("测验导入异常:{}", ExceptionUtils.getRootCauseMessage(e));
            return;
        } catch (BusinessException e) {
            logger.error("测验导入异常:{}", ExceptionUtils.getRootCauseMessage(e));
            String fileName = StringUtils.defaultIfBlank(excel.getOriginalFilename(), StringUtils.EMPTY);
            // tips 如果是读取文件或者获取量表信息异常，则直接记录并抛出异常
            psyImportMapper.insert(
                    TblScalePsyImport.builder()
                            .status(-1)
                            .errMsg(StringUtils.substring(String.join(":",fileName, e.getMessage()), 0, 4000))
                            .build());
            return;
        }

        /*
         * 处理导入流程
         */
        try {
            this.handle(gaugeNameRow, sheet, scale.getId(), scale.getScaleName());
        } catch (Exception e) {
            try {
                 filePath = FileUploadUtils.upload(FileServerConfig.getImportPath(), excel, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
            } catch (IOException ignored) {} catch (InvalidExtensionException ex) {
                throw new RuntimeException(ex);
            }
            logger.error("PsyImportProcess.handle异常：{}" , ExceptionUtils.getStackTrace(e));
            // 失败日志
            psyImportMapper.insert(
                    TblScalePsyImport.builder()
                            .scaleName(scale.getScaleName())
                            .errMsg(e.getMessage())
                            .status(0)
                            .fileUrl(filePath)
                            .build());
        }
    }

    public void handle(int gaugeNameRow, Sheet sheet, String scaleId, String scaleName) throws SystemException {
        // 获取档案信息
        TblPatient patient = getPatientBasic(gaugeNameRow, sheet);
        // 初始化测验记录
        LocalDateTime now = LocalDateTime.now();
        TblScalePsyRecord psyRecord = TblScalePsyRecord.builder()
                .scaleId(scaleId)
                .patId(patient.getId())
                .psyType(0)
                .executor(ShiroUtil.getAuthUserInfo().getUserId())
                .createBy(ShiroUtil.getAuthUserInfo().getUserId())
                .createTime(now)
                .build();
        // 初始化报告，暂时不设置分配记录id
        TblScaleReport report = new TblScaleReport();
        report.setPatId(patient.getId());
        report.setScaleId(scaleId);
        report.setStartTime(now);
        report.setPatName(patient.getPatName());
        // 这里存的是字典值
        report.setPatSex(patient.getPatSex());
        report.setPatAge(patient.getPatAge());
        report.setPatBirthday(patient.getPatBirthday());
        report.setPatMobile(patient.getPatMobile());
        report.setSubmissionTime(LocalDateTime.now());
        report.setReportStatus(0);

        report.setDelFlag(DeleteStatus.NORMAL);

        // 初始化提交参数
        SubmitGaugeBo bo = new SubmitGaugeBo();
        // 档案信息
        bo.setPatient(patientService.getSubmitUserInfo(patient.getId()));
        // 量表编号
        bo.setGaugeId(scaleId);
        // 生成作答所需要的参数(formData, 作答记录)
        generateSubmitParameter(bo, sheet, gaugeNameRow, scaleId);

        // 新增分配记录
        psyRecordService.insert(psyRecord);

        // 新增报告记录
        report.setAnswerJson(JSON.toJSONString(bo.getFormData()));
        report.setAllotId(psyRecord.getId());
        reportService.insert(report);

        // step 6.提交作答
        psyAnswerService.afterSubmit(
                bo,
                report::getId,
                e -> gaugeAnswerService.submit(report.getId(), GaugeType.SCALE, e));

        // step 7.创建量表导入记录
        psyImportMapper.insert(
                TblScalePsyImport.builder()
                        .patId(patient.getId())
                        .patName(patient.getPatName())
                        .patSex(patient.getPatSex())
                        .patAge(patient.getPatAge())
                        .patId(patient.getId())
                        .rId(report.getId())
                        .scaleName(scaleName)
                        .status(1)
                        .build());
    }

    /**
     * TODO 读取档案基本信息
     */
    private TblPatient getPatientBasic(int gaugeNameRow, Sheet sheet) {
        TblPatient patient;
        // step 3.1根据量表的模板编号获取档案模板
        List<PatientItem> patientItems = Arrays.stream(PatientModule.values())
                .map(PatientModule::getItem)
                .collect(Collectors.toList());
        // 模板转化为 map {姓名：模板， 性别：模板 ...}
        Map<String, PatientItem> archiveModuleMap = patientItems
                .stream()
                .collect(Collectors.toMap(PatientItem::getLabel, module -> module, (k1, k2) -> k1));
        // step 4.根据档案信息获取档案
        Map<String, Object> archiveMap = new HashMap<>();
        for (int i = 1; i < gaugeNameRow - 1; i++) {
            PatientItem archiveModule;
            for (int j = 0; j < gaugeExcelMaxCol / 2; j++) {
                int finalJ = j * 2;
                archiveModule = archiveModuleMap.get(Optional.ofNullable(sheet.getRow(i)).map(row -> row.getCell(finalJ)).map(Cell::getStringCellValue).orElse(""));
                if (null == archiveModule) {
                    continue;
                }
                Object data = null;
                if (StringUtils.equals("text", archiveModule.getType())) {
                    data = Optional.ofNullable(sheet.getRow(i)).map(row -> row.getCell(finalJ + 1)).map(Cell::getStringCellValue).orElse(null);
                } else if (StringUtils.equals("select", archiveModule.getType())) {
                    if (StringUtils.isNotBlank(archiveModule.getDictCode())) {
                        Map<String, String> dictionaryMap = dictDataService.selectDictDataByType(archiveModule.getDictCode())
                                .stream()
                                .collect(Collectors.toMap(Option::getLabel, Option::getValue, (k1, k2) -> k1));
                        String key = Optional.ofNullable(sheet.getRow(i)).map(row -> row.getCell(finalJ + 1)).map(Cell::getStringCellValue).orElse("");
                        data = dictionaryMap.get(key);
                    } else {
                        data = Optional.ofNullable(sheet.getRow(i)).map(row -> row.getCell(finalJ + 1)).map(Cell::getStringCellValue).orElse("");
                    }
                } else if (StringUtils.equals("date", archiveModule.getType())) {
                    data = Optional.ofNullable(sheet.getRow(i)).map(row -> row.getCell(finalJ + 1)).map(Cell::getDateCellValue).orElse(null);
                }
                // 如果必填，校验内容是否为null
                if (archiveModule.isRequired() && (data == null || StringUtils.isBlank(String.valueOf(data)))) {
                    throw new SystemException("请补全" + archiveModule.getLabel() + "信息");
                }
                archiveMap.put(archiveModule.getName(), data);
            }
        }
        // 档案map转化为实体
        patient = BeanUtil.mapToBean(archiveMap, TblPatient.class, true);
        // 判断档案是否存在
        TblPatient target;
        if (StringUtils.isBlank(patient.getPatIdentNo()) || null == (target = patientService.selectByNo(patient))) {
            // 密码
            try {
                patientService.insert(patient);
            } catch (ConstraintViolationException e1) {
                logger.error(e1.getMessage());
                throw new SystemException(e1.getConstraintViolations().stream()
                        .map(ConstraintViolation::getMessage)
                        .collect(Collectors.joining(";")));
            } catch (Exception e) {
                logger.error(e.getMessage());
                throw new SystemException("档案创建失败");
            }
        } else {
            patient = target;
        }
        if (Objects.nonNull(patient.getPatBirthday())) {
            patient.setPatAge("" + DateUtil.ageOfNow(patient.getPatBirthday().toString()));
        }
        return patient;
    }

    /**
     * TODO 组装提交作答所需参数（表单数据、原始答卷）
     */
    private void generateSubmitParameter(SubmitGaugeBo bo, Sheet sheet, int gaugeNameRow, String gaugeNum) {
        Map<String, Object> formData = new HashMap<>();
        List<TblGaugeAnswer> list = new ArrayList<>();
        List<TblGaugeQuestion> questions = questionMapper.selectQutWithOptWithLogicByGaugeId(gaugeNum, GaugeType.SCALE);
        Map<Integer, Map<String, TblGaugeOption>> questMaps = questions.stream().collect(Collectors.toMap(
                TblGaugeQuestion::getQutSort, v -> v.getOptions()
                        .stream()
                        .collect(Collectors.toMap(TblGaugeOption::getOptSymbol, val -> val)), (a, b) -> a));
        // 初始化题目游标
        int cursor = gaugeNameRow + 1;
        for (int i = 0; i < questions.size(); i++) {
            int finalI = i;
            if (finalI > 0) {
                cursor += questions.get(finalI - 1).getOptions().size() + 1;
            }
            // 获取题目序号
            Integer questionNum = Optional.ofNullable(sheet.getRow(cursor))
                    .map(row -> row.getCell(0))
                    .map(Cell::getNumericCellValue)
                    .map(Double::intValue)
                    .orElseThrow(() -> new SystemException(String.format("第[%d]题在当前量表中不存在，请检查后在导入", (finalI + 1))));
            // 获取填写的答案
            String answer1 = Optional.ofNullable(sheet.getRow(cursor))
                    .map(row -> row.getCell(gaugeExcelMaxCol - 1))
                    .map(Cell::getStringCellValue)
                    .orElseThrow(() -> new SystemException(String.format("第[%d]题选项未作答，请检查后在导入", (finalI + 1))));
            if (StringUtils.isBlank(answer1)) {
                throw new SystemException(String.format("第[%d]题未作答，请检查后在导入", (finalI + 1)));
            }
            // 根据填写的答案查询对应的选项
            TblGaugeOption option = Optional.ofNullable(questMaps.get(questionNum))
                    .map(m -> m.get(answer1))
                    .orElseThrow(() -> new SystemException(String.format("第[%d]题选项与当前量表不匹配，请检查后在导入", (finalI + 1))));
            // 组装formData
            formData.put(questions.get(i).getName(), option.getOptValue());
            TblGaugeAnswer answer = TblGaugeAnswer.builder()
                    .qutId(questions.get(finalI).getQutId())
                    .optId(option.getOptId())
                    .answer(String.valueOf(option.getOptValue()))
                    .type(GaugeType.SCALE)
                    .build();
            list.add(answer);
        }
        bo.setFormData(formData);
        bo.setAnswers(list);
    }
}
